/**
 * Copyright (C) 2008 Mathieu Carbou <mathieu.carbou@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.code.xmltool;

import com.google.code.xmltool.util.XMLErrorHandler;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import java.io.*;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URISyntaxException;
import java.net.URL;

/**
 * @author Mathieu Carbou (mathieu.carbou@gmail.com)
 */
public final class XMLDocBuilder {

    private final XMLDocDefinition definition = new XMLDocDefinition(newDocumentBuilder().newDocument());

    private XMLDocBuilder() {
    }

    /**
     * Add a namespace to the document that will be created
     *
     * @param prefix       The prefix of the namespace
     * @param namespaceURI The URI of the namespace
     * @return this
     */
    public XMLDocBuilder addNamespace(String prefix, String namespaceURI) {
        definition.addNamespace(prefix, namespaceURI);
        return this;
    }

    /**
     * Set the default namespace to use in the document declaration. I.e.
     * <code>&lt;html xmlns="http://www.w3.org/1999/xhtml"></code>
     *
     * @param defaultNamespaceURI URI to use as default when tags are not prefixed
     * @return this
     */
    public XMLDocBuilder addDefaultNamespace(String defaultNamespaceURI) {
        definition.addDefaultNamespace(defaultNamespaceURI);
        return this;
    }

    /**
     * Create a root node for this XML document
     *
     * @param tagName Name of the element
     * @return XMLDoc instance to build and navigate in the document
     */
    public XMLDocument addRoot(String tagName) {
        return create(definition.createRoot(tagName));
    }

    private static XMLDocument create(final XMLDocDefinition def) {
        def.normalize();
        final XMLDocument doc = new XMLDoc(def);
        InvocationHandler handler = new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                def.normalize();
                try {
                    return method.invoke(doc, args);
                }
                catch (InvocationTargetException e) {
                    throw e.getTargetException();
                }
            }
        };
        return (XMLDocument) Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class<?>[]{XMLDocument.class},
                handler);
    }

    static XMLDocBuilder newDocument() {
        return new XMLDocBuilder();
    }

    static XMLDocument from(Node node) {
        return create(new XMLDocDefinition(node));
    }

    static XMLDocument from(InputSource source) {
        try {
            return from(newDocumentBuilder().parse(source));
        }
        catch (Exception e) {
            throw new XMLDocumentException("Error creating XMlDoc. Please verify that the input source can be read and is well formed", e);
        }
    }

    static XMLDocument from(Reader reader) {
        XMLDocument doc = from(new InputSource(reader));
        close(reader);
        return doc;
    }

    static XMLDocument from(InputStream is) {
        XMLDocument doc = from(new InputSource(is));
        close(is);
        return doc;
    }

    static XMLDocument from(File file) {
        return from(new InputSource(file.toURI().toASCIIString()));
    }

    static XMLDocument from(URL xmlLocation) {
        try {
            return from(new InputSource(xmlLocation.toURI().toASCIIString()));
        }
        catch (URISyntaxException e) {
            throw new XMLDocumentException("URL Syntax error in " + xmlLocation, e);
        }
    }

    static XMLDocument from(String xmlData) {
        return from(new StringReader(xmlData));
    }

    static XMLDocument from(Source source) {
        try {
            TransformerFactory tf = TransformerFactory.newInstance();
            Transformer transformer = tf.newTransformer();
            transformer.setErrorListener(new XMLErrorHandler(true));
            DOMResult res = new DOMResult();
            transformer.transform(source, res);
            return from(res.getNode());
        }
        catch (TransformerException e) {
            throw new XMLDocumentException("Error creating XMlDoc. Please verify that the input source can be read and is well formed", e);
        }
    }

    private static void close(Closeable c) {
        try {
            c.close();
        }
        catch (IOException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }

    private static DocumentBuilder newDocumentBuilder() {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            DocumentBuilder builder = factory.newDocumentBuilder();
            builder.setErrorHandler(new XMLErrorHandler(true));
            return builder;
        }
        catch (ParserConfigurationException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }
}